﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.ServiceModel;
using System.ServiceModel.Description;

namespace ServerNodeLib
{
	/// <summary>
	/// This is the server component.
	/// 
	/// It is independent of the GUI.  All communication occurs through the server.
	/// </summary>
	[ServiceBehaviorAttribute(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
	public class RoundRobinSchedulerServer : ISchedulerServer
	{
		#region data members
		private int idleNodes;
		public int IdleNodes
		{
			get
			{
				return this.idleNodes;
			}
		}
		public int TotalNodes
		{
			get
			{
				return this.nodes.Rows.Count;
			}
		}
		private DataTable nodes;
		public DataTable Nodes
		{
			get
			{
				return this.nodes;
			}
			set
			{
				if (value != null)
					this.nodes = value;
			}
		}
		

		private int queuedBatches;
		public int QueuedBatches
		{
			get
			{
				return this.queuedBatches;
			}
		}
		public int TotalBatches
		{
			get
			{
				return this.batches.Rows.Count;
			}
		}
		private DataTable batches;
		public DataTable Batches
		{
			get
			{
				return this.batches;
			}
			set
			{
				if (value != null)
					this.batches = value;
			}
		}		
		

		private System.Security.Cryptography.MD5 md5;
		private System.Threading.Thread updateThread;
		private  ServiceHost SchedulerServerHost;
		/// <summary>
		/// Delegate for notifying the server that we've completed a refresh loop
		/// </summary>
		public Action OnStateChanged;
		public Action OnServerToggled;
		/// <summary>
		/// Necessary to fix bug where selecting an xml file
		/// in a different directory causes the batches.xml file to be saved there
		/// </summary>
		private String executionPath;
		#endregion

		#region class and thread management
		public RoundRobinSchedulerServer()
		{
			BuildNodesTable();
			BuildBatchesTable();
			
			queuedBatches = 0;
			idleNodes = 0;
			md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();

			ServerNodeSettings.Default.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(Default_PropertyChanged);

			executionPath = System.IO.Directory.GetCurrentDirectory();
			if (executionPath == null)
				executionPath = String.Empty;
		}

		private void BuildNodesTable()
		{
			this.nodes = new DataTable("nodes");
			nodes.Columns.Add(NodesTableKeys.BatchId, typeof(Guid));
			nodes.Columns.Add(NodesTableKeys.Status, typeof(NodeStatus));
			nodes.Columns.Add(NodesTableKeys.LastSeen, typeof(DateTime));
			// Queue doesn't serialize; have to use List
			nodes.Columns.Add(NodesTableKeys.NextAction, typeof(List<ActionRequest>));
			nodes.Columns.Add(NodesTableKeys.NodeId, typeof(Guid));
		}

		private void BuildBatchesTable()
		{
			this.batches = new DataTable("batches");
			batches.Columns.Add(BatchTableKeys.BatchId, typeof(Guid));
			batches.Columns.Add(BatchTableKeys.BatchObject, typeof(Batch));
			batches.Columns.Add(BatchTableKeys.BatchDuration, typeof(TimeSpan));
		}

		public void Start()
		{
			try
			{
				if (SchedulerServerHost != null)
				{
					SchedulerServerHost.Close();
				}
				SchedulerServerHost = new ServiceHost(this, new Uri(ServerNodeSettings.Default.SchedulerServerWcfUrl));
				NetTcpBinding tcpBinding = new NetTcpBinding(SecurityMode.None);
				//tcpBinding.MaxConnections = 50;

				SchedulerServerHost.AddServiceEndpoint(
					typeof(ISchedulerServer),
					tcpBinding,
					String.Empty
				);

				ServiceMetadataBehavior smb = SchedulerServerHost.Description.Behaviors.Find<ServiceMetadataBehavior>();
				// If not, add one
				if (smb == null)
					smb = new ServiceMetadataBehavior();

				smb.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
				//smb.HttpGetEnabled = true;

				SchedulerServerHost.Description.Behaviors.Add(smb);
				SchedulerServerHost.AddServiceEndpoint(
				  ServiceMetadataBehavior.MexContractName,
				  MetadataExchangeBindings.CreateMexTcpBinding(),
				  "mex"
				);

				SchedulerServerHost.Open();
				if (updateThread != null && updateThread.IsAlive)
					updateThread.Abort();
				updateThread = new System.Threading.Thread(this.RefreshLoop);
				updateThread.Start();
				if (OnServerToggled != null)
					this.OnServerToggled.Invoke();
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message);
				this.Stop();
			}
		}

		public void Stop()
		{
			try
			{
				if (updateThread != null)
					updateThread.Abort();
				if (SchedulerServerHost != null)
					SchedulerServerHost.Close();
				if (OnServerToggled != null)
					this.OnServerToggled.Invoke();
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message);
			}
		}

		public bool IsRunning()
		{
			if (updateThread == null || SchedulerServerHost == null)
				return false;
			if (updateThread.IsAlive == false)
				return false;
			if (SchedulerServerHost.State != CommunicationState.Opened)
				return false;
			return true;
		}

		#endregion

		#region ISchedulerServer Members

		public Guid AnnounceMyself(string name)
		{
			if (String.IsNullOrEmpty(name))
				return Guid.Empty;
			try
			{
				lock (nodes)
				{
					foreach (DataRow row in nodes.Rows)
					{
						NodeStatus status = row[NodesTableKeys.Status] as NodeStatus;
						if (status.NodeName.Equals(name))
						{
							return status.NodeId;
						}
					}
					NodeStatus newStatus = new NodeStatus();
					newStatus.NodeId = Guid.NewGuid();
					newStatus.NodeName = name;
					newStatus.State = NodeState.Announced;
					DataRow newRow = nodes.NewRow();
					newRow[NodesTableKeys.NodeId] = newStatus.NodeId;
					newRow[NodesTableKeys.Status] = newStatus;
					newRow[NodesTableKeys.LastSeen] = DateTime.Now;
					newRow[NodesTableKeys.BatchId] = Guid.Empty;
					newRow[NodesTableKeys.NextAction] = new List<ActionRequest>();
					(newRow[NodesTableKeys.NextAction] as List<ActionRequest>).Add(new ActionRequest(ActionRequestType.NoAction));
					nodes.Rows.Add(newRow);
					return newStatus.NodeId;
				}
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message);
				return Guid.Empty;
			}
		}

		public bool UnAnnounceMyself(Guid nodeId)
		{
			try
			{
				lock (nodes)
				{
					DataRow removeRow = null;
					foreach (DataRow row in nodes.Rows)
					{
						NodeStatus status = row[NodesTableKeys.Status] as NodeStatus;
						if (status.NodeId.Equals(nodeId))
						{
							Batch b = GetBatch(status.BatchId);
							if (b != null)
							{
								lock (b)
								{
									b.NodeId = Guid.Empty;
								}
							}
							removeRow = row;
							break;
						}
					}
					if (removeRow == null)
					{
						return false;
					}
					else
					{
						nodes.Rows.Remove(removeRow);
						return true;
					}
				}
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message);
				return false;
			}
		}

		public ActionRequest SetStatus(Guid nodeId, NodeStatus newStatus)
		{
			try
			{
				if (newStatus == null)
					return null;
				ActionRequest responseRequest = new ActionRequest(ActionRequestType.AnnounceNode);

				lock (nodes)
				{
					// don't completely trust the client code
					newStatus.NodeId = nodeId;
					foreach (DataRow row in nodes.Rows)
					{
						NodeStatus status = row[NodesTableKeys.Status] as NodeStatus;
						if (status == null)
							continue;
						// if the client is doing a different batch than we think it is, kill it
						Batch b = GetBatch(newStatus.BatchId);
						if (b != null && b.NodeId.Equals(nodeId) == false)
						{
							return new ActionRequest(ActionRequestType.KillJob);
						}

						if (status.NodeId.Equals(nodeId))
						{
							row.BeginEdit();
							row[NodesTableKeys.BatchId] = newStatus.BatchId;
							row[NodesTableKeys.Status] = newStatus;
							row[NodesTableKeys.LastSeen] = DateTime.Now;
							List<ActionRequest> responseList = row[NodesTableKeys.NextAction] as List<ActionRequest>;
							if (responseList == null || responseList.Count == 0)
							{
								responseList = new List<ActionRequest>();
								responseList.Add(new ActionRequest(ActionRequestType.NoAction));
							}
							responseRequest = responseList[0];
							responseList.RemoveAt(0);

							if (responseRequest.ActionType == ActionRequestType.NoAction && newStatus.State == NodeState.Idle)
							{
								// check if there is another batch to download
								// now the client can choose!
								if (queuedBatches > 0 && newStatus.AcceptNewJobs)
									responseRequest.ActionType = ActionRequestType.GetJob;
							}
							row.EndEdit();
							row.AcceptChanges();
						}
					}
				}
				return responseRequest;
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message);
				return new ActionRequest(ActionRequestType.Error);
			}
		}

		public NodeStatus GetStatus(Guid clientGuid)
		{
			return GetNode(clientGuid);
		}

		public List<Guid> GetAllNodes()
		{
			List<Guid> retList = new List<Guid>();
			lock (nodes)
			{
				foreach (DataRow row in nodes.Rows)
				{
					NodeStatus status = row[NodesTableKeys.Status] as NodeStatus;
					retList.Add(status.NodeId);
				}
			}
			return retList;
		}

		public BatchInfo GetAvailableBatchJob(Guid clientGuid)
		{
			try
			{
				lock (batches)
				{
					foreach (DataRow row in batches.Rows)
					{
						Batch b = row[BatchTableKeys.BatchObject] as Batch;
						lock (b)
						{
							if (b.IsQueued())
							{
								NodeStatus status = GetNode(clientGuid);
								if (status == null)
									return null;
								if (status.BatchId.Equals(Guid.Empty) == false)
								{
									Batch otherBatch = GetBatch(status.BatchId);
									status.BatchId = Guid.Empty;
									if (otherBatch != null)
									{
										lock (otherBatch)
										{
											otherBatch.NodeId = Guid.Empty;
											otherBatch.NodeBegin = DateTime.MinValue;
										}
									}
								}
								b.NodeId = status.NodeId;
								b.NodeBegin = DateTime.Now;
								return BuildBatchInfo(b);
							}
						}
					}
				}
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message + "@" + ex.StackTrace);
			}
			return null;
		}


		public byte[] GetResourceChunk(Guid batchId, string destName, int chunkNum)
		{
			try
			{
				BatchResource resource = GetBatchResource(batchId, destName);
				if (resource == null || System.IO.File.Exists(resource.ServerPath) == false)
					return null;// throw new System.IO.FileNotFoundException(ExceptionMessages.ResourceFileNotFound, destName);
				lock (resource)
				{


					using (System.IO.FileStream fs = new System.IO.FileStream(resource.ServerPath, System.IO.FileMode.Open))
					{
						int chunks = (int)(fs.Length / ServerNodeSettings.Default.TransferChunkSize);
						if (chunkNum > chunks || chunkNum < 0)
							return null;
						//throw new ArgumentException(String.Format(ExceptionMessages.ChunkNumInvalid, 0, chunks, chunkNum));

						int offset = chunkNum * ServerNodeSettings.Default.TransferChunkSize;
						int dataSize = 0;
						if (chunkNum == chunks)
						{
							dataSize = (int)fs.Length - offset;
						}
						else
						{
							dataSize = ServerNodeSettings.Default.TransferChunkSize;
						}

						byte[] retData = new byte[dataSize];

						fs.Seek(offset, System.IO.SeekOrigin.Begin);
						int readData = fs.Read(retData, 0, retData.Length);
						if (readData != dataSize)
							return null;
						//throw new System.IO.InvalidDataException(String.Format(ExceptionMessages.InsufficientDataRead, destName, readData, dataSize));

						return retData;
					}
				}
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message + "@" + ex.StackTrace);
				return null;
			}
		}

		public Guid PostResult(Guid batchId, ResultInfo result)
		{
			try
			{
				Batch theBatch = GetBatch(batchId);
				if (theBatch == null)
				{
					return Guid.Empty;
				}
				//	throw new ArgumentException(ExceptionMessages.BatchNotFound, "batchId");
				if (result == null)
					return Guid.Empty;
				//throw new ArgumentNullException("result", ExceptionMessages.ArgumentNull);

				Guid postResultGuid = Guid.NewGuid();

				theBatch.NodeEnd = DateTime.Now;
				theBatch.Result = result;

				BatchGroup group = theBatch.BatchGroup;
				NodeStatus status = GetNode(theBatch.NodeId);

				if (result.IsSuccess)
				{
					String resultFileName = theBatch.LogFilenameFormat;
					if(System.IO.Path.GetExtension(resultFileName).Equals(".xml") == false)
						resultFileName += ".xml";
					/*
					if(status!= null)
					{
						resultFileName = String.Format(ServerNodeSettings.Default.GroupResultFilenameFormat,status.NodeName, result.BatchId);
					}
					*/
					String destPath = System.IO.Path.Combine(ServerNodeSettings.Default.ResultResourceDirectory, group.GetDirectoryName());
					if (System.IO.Directory.Exists(destPath) == false)
						System.IO.Directory.CreateDirectory(destPath);
					String serializedPath = System.IO.Path.Combine(destPath, resultFileName);
					if (System.IO.File.Exists(serializedPath))
					{
						serializedPath = System.IO.Path.Combine(destPath, Guid.NewGuid().ToString() + ".xml");
					}

					using (System.IO.StreamWriter writer = new System.IO.StreamWriter(serializedPath))
					{
						System.Xml.Serialization.XmlSerializer s = new System.Xml.Serialization.XmlSerializer(typeof(Batch));
						s.Serialize(writer, theBatch);
						writer.Close();
					}

					serializedPath = System.IO.Path.Combine(destPath, ServerNodeSettings.Default.GroupInfoFileName);

					lock (group)
					{
						group.CompleteBatches++;
						if (group.TotalBatches == group.CompleteBatches)
							group.EndTime = DateTime.Now;
						System.Xml.Serialization.XmlSerializer xsg = new System.Xml.Serialization.XmlSerializer(typeof(BatchGroup));
						using (System.IO.StreamWriter w = new System.IO.StreamWriter(serializedPath))
						{
							xsg.Serialize(w, group);
							w.Close();
						}
					}

					theBatch.NodeId = Guid.Empty;
					idleNodes--;
					lock (batches)
					{
						DataRow removeRow = null;
						foreach (DataRow row in batches.Rows)
						{
							Guid g = (Guid)row[BatchTableKeys.BatchId];
							if (g.Equals(theBatch.BatchId))
							{
								removeRow = row;
								break;
							}
						}
						if (removeRow != null)
							batches.Rows.Remove(removeRow);
					}


				}
				else
				{
					CommonLib.Logger.Instance.Report(CommonLib.LogType.Event,
						this,
						String.Format(ExceptionMessages.BatchJobNotSuccess, theBatch.BatchId, theBatch.NodeId, result.SummaryMessage));

					theBatch.NodeId = Guid.Empty;
					theBatch.NodeBegin = DateTime.MinValue;
					theBatch.NodeEnd = DateTime.MaxValue;
					theBatch.Result = null;
				}

				return postResultGuid;
			}catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message + "@" + ex.StackTrace);
			}
			return Guid.Empty;
		}

		public bool PostResourceChunk(Guid batchId, String clientPath, int chunkNum, byte[] data)
		{
			try
			{
				Batch b = GetBatch(batchId);
				if (b == null)
				{
					b = new Batch();
					b.BatchGroup = new BatchGroup();
					b.BatchId = Guid.Empty;
				}
				//return false;
				//throw new ArgumentException(ExceptionMessages.BatchNotFound, "batchId");
				String serverPath = null;
				foreach (BatchResource res in b.ReturnResources)
				{
					if (res.ClientPath.Equals(clientPath))
						serverPath = res.ServerPath;
				}
				if (String.IsNullOrEmpty(serverPath))
				{
					serverPath = System.IO.Path.GetFileName(batchId.ToString() + " " + clientPath);
				}
				//throw new ArgumentException(ExceptionMessages.ResourceFileNotFound, "clientPath");

				BatchGroup group = b.BatchGroup;
				String destPath = System.IO.Path.Combine(ServerNodeSettings.Default.ResultResourceDirectory, group.GetDirectoryName());
				if (System.IO.Directory.Exists(destPath) == false)
					System.IO.Directory.CreateDirectory(destPath);
				destPath = System.IO.Path.Combine(destPath, System.IO.Path.GetFileName(serverPath));

				System.IO.FileMode mode = System.IO.FileMode.Create;
				if (chunkNum != 0)
					mode = System.IO.FileMode.Append;

				using (System.IO.FileStream fs = new System.IO.FileStream(destPath, mode))
				{
					fs.Write(data, 0, data.Length);
					fs.Close();
				}
				return true;
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message + "@" + ex.StackTrace);
			}
			return false;
		}

		#endregion

		#region Gui interface points
		public void KillBatchOnNode(Guid nodeId)
		{
			lock (nodes)
			{
				foreach (DataRow row in nodes.Rows)
				{
					Guid g = (Guid)row[NodesTableKeys.NodeId];
					if (g.Equals(nodeId))
					{
						(row[NodesTableKeys.NextAction] as List<ActionRequest>).Add(new ActionRequest(ActionRequestType.KillJob));
						return;
					}
				}
			}
		}

		public void KillNode(Guid nodeId)
		{
			List<DataRow> toRemove = new List<DataRow>();
			lock (nodes)
			{
				foreach (DataRow row in nodes.Rows)
				{
					Guid g = (Guid)row[NodesTableKeys.NodeId];
					if (g.Equals(nodeId))
						toRemove.Add(row);
				}
				foreach (DataRow row in toRemove)
				{
					nodes.Rows.Remove(row);
				}
			}
		}

		public void KillBatch(Guid batchId)
		{
			List<DataRow> toRemove = new List<DataRow>();
			lock (batches)
			{
				foreach (DataRow row in batches.Rows)
				{
					Guid g = (Guid)row[BatchTableKeys.BatchId];
					if (g.Equals(batchId))
						toRemove.Add(row);
				}
				foreach (DataRow row in toRemove)
				{
					batches.Rows.Remove(row);
				}
			}
		}

		public Guid GetNodeRunningBatch(Guid batchId)
		{
			Batch b = GetBatch(batchId);
			if (b == null)
				return Guid.Empty;

			return b.NodeId;
		}
		/// <summary>
		/// Resets the state of the specified batch.
		/// Useful if a node disappears and you want the batch to be
		/// assigned out to a different node.
		/// </summary>
		/// <param name="batchId"></param>
		public void ResetBatch(Guid batchId)
		{
			Batch b = GetBatch(batchId);
			if (b == null)
				return;
			lock (b)
			{
				KillBatchOnNode(b.NodeId);
				b.NodeId = Guid.Empty;
				b.Result = null;
				b.NodeBegin = DateTime.MinValue;
				b.NodeEnd = DateTime.MaxValue;
			}
		}
		/// <summary>
		/// Enqueues an action to be delivered to the node next status update
		/// </summary>
		/// <param name="nodeId">The node to request</param>
		/// <param name="req">The specific request</param>
		public void EnqueueActionRequest(Guid nodeId, ActionRequest req)
		{
			if (req == null)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ExceptionMessages.ArgumentNull + "req");
				return;
			}
			lock (nodes)
			{
				foreach (DataRow row in nodes.Rows)
				{
					Guid g = (Guid)row[NodesTableKeys.NodeId];
					if (g.Equals(nodeId))
					{
						(row[NodesTableKeys.NextAction] as List<ActionRequest>).Add(req);
						return;
					}
				}
			}
		}

		public ServerNodeSettings Settings
		{
			get
			{
				return ServerNodeSettings.Default;
			}
		}

		void Default_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
		{
			ServerNodeSettings.Default.Save();
		}

		#endregion

		#region helper methods

		/// <summary>
		/// Background thread updates the state of the server.
		/// I do this periodically because it's just not important
		/// enough to do continually. Plus, large node numbers make event
		/// handlers a little heavy.
		/// </summary>
		private void RefreshLoop()
		{
			while (true)
			{
				try
				{
					CheckNodeTimeouts();
					CountQueuedBatches();
					CountIdleNodes();
					if (OnStateChanged != null)
						OnStateChanged.Invoke();
					//SaveState();
					System.Threading.Thread.Sleep(ServerNodeSettings.Default.RefreshLoopSleep);
				}
				catch (System.Threading.ThreadAbortException)
				{
					return;
				}
				catch (Exception ex)
				{
					CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message + "@" + ex.StackTrace);
					System.Threading.Thread.Sleep(ServerNodeSettings.Default.RefreshLoopSleep);
				}
				finally
				{

				}
			}
		}

		public void SaveState()
		{
			try
			{
				System.Xml.Serialization.XmlSerializer dtS = new System.Xml.Serialization.XmlSerializer(typeof(DataTable));
				lock (batches)
				{

					using (System.IO.StreamWriter writer = new System.IO.StreamWriter(System.IO.Path.Combine(executionPath, "batches.xml")))
					{
						dtS.Serialize(writer, batches);
					}
				}

				lock (nodes)
				{
					using (System.IO.StreamWriter writer = new System.IO.StreamWriter(System.IO.Path.Combine(executionPath, "nodes.xml")))
					{
						dtS.Serialize(writer, nodes);
					}
				}
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message);
			}
		}

		public void LoadState()
		{
			try
			{
				System.Xml.Serialization.XmlSerializer dtS = new System.Xml.Serialization.XmlSerializer(typeof(DataTable));

				if (System.IO.File.Exists(System.IO.Path.Combine(executionPath, "batches.xml")))
				{
					using (System.IO.StreamReader reader = new System.IO.StreamReader(System.IO.Path.Combine(executionPath, "batches.xml")))
					{
						batches = (DataTable)dtS.Deserialize(reader);
					}
				}

				if (System.IO.File.Exists(System.IO.Path.Combine(executionPath, "nodes.xml")))
				{
					using (System.IO.StreamReader reader = new System.IO.StreamReader(System.IO.Path.Combine(executionPath, "nodes.xml")))
					{
						nodes = (DataTable)dtS.Deserialize(reader);
					}
				}
			}
			catch (Exception ex)
			{
				CommonLib.Logger.Instance.Report(CommonLib.LogType.Error, this, ex.Message);
			}
			finally
			{
				if (batches == null)
					BuildBatchesTable();
				if (nodes == null)
					BuildNodesTable();
			}
		}

		/// <summary>
		/// Checks nodes for timeouts!
		/// Updates batches object if a timeout is detected.
		/// </summary>
		private void CheckNodeTimeouts()
		{
			lock (nodes)
			{
				DateTime now = DateTime.Now;
				List<DataRow> nodeTimesouts = new List<DataRow>();
				foreach (DataRow row in nodes.Rows)
				{
					DateTime lastSeen = (DateTime) row[NodesTableKeys.LastSeen];
					if (now.Subtract(lastSeen).CompareTo(ServerNodeSettings.Default.NodeTimeout) > 0)
					{
						nodeTimesouts.Add(row);
					}
				}
				foreach (DataRow row in nodeTimesouts)
				{
					Guid batchId = (Guid)row[NodesTableKeys.BatchId];
					Batch b = GetBatch(batchId);
					if (b != null)
					{
						b.NodeId = Guid.Empty;
					}
					
					//row.Delete();
					nodes.Rows.Remove(row);
				}
			}
		}

		private void CountQueuedBatches()
		{
			queuedBatches = 0;
			DateTime now = DateTime.Now;
			lock (batches)
			{
				foreach (DataRow row in batches.Rows)
				{
					Batch b = row[BatchTableKeys.BatchObject] as Batch;
					if (b.IsQueued())
						queuedBatches++;
					row.BeginEdit();
					row[BatchTableKeys.BatchDuration] = now.Subtract(b.NodeBegin);
					row.EndEdit();
				}
			}
		}

		private void CountIdleNodes()
		{
			idleNodes = 0;
			lock (nodes)
			{
				foreach (DataRow row in nodes.Rows)
				{
					Guid batchId = (Guid)row[NodesTableKeys.BatchId];
					if (batchId.Equals(Guid.Empty))
						idleNodes++;
				}
			}
		}

		private NodeStatus GetNode(Guid nodeId)
		{
			lock (nodes)
			{
				foreach (DataRow row in nodes.Rows)
				{
					Guid g = (Guid)row[NodesTableKeys.NodeId];
					if (g.Equals(nodeId))
						return row[NodesTableKeys.Status] as NodeStatus;
				}
			}
			return null;
		}

		private Batch GetBatch(Guid batchId)
		{
			lock (batches)
			{
				foreach (DataRow row in batches.Rows)
				{
					Batch b = row[BatchTableKeys.BatchObject] as Batch;
					if (b.BatchId.Equals(batchId))
						return b;
				}
			}
			return null;
		}

		private BatchResource GetBatchResource(Guid batchId, String destName)
		{
			if (String.IsNullOrEmpty(destName))
				return null;
			if (batchId.Equals(Guid.Empty))
				return null;

			Batch theBatch = GetBatch(batchId);
			if (theBatch == null)
				return null;
			ResourceInfo info = new ResourceInfo();
			info.TransferChunkSize = ServerNodeSettings.Default.TransferChunkSize;

			BatchResource targetRes = null;

			lock (theBatch)
			{
				foreach (BatchResource br in theBatch.BatchResources)
				{
					if (br.ClientPath.ToLowerInvariant().Equals(destName.ToLowerInvariant()))
					{
						targetRes = br;
						break;
					}
				}
			}
			return targetRes;
		}
		/// <summary>
		/// Given a server-defined Batch, this builds a BatchInfo object for
		/// a node to consume.
		/// 
		/// I reocmmend doing this each time a node requests a new batch.
		/// The reason is that hashes may change or files may disappear when you
		/// least expect it.
		/// </summary>
		/// <param name="b">The batch object</param>
		/// <returns></returns>
		protected BatchInfo BuildBatchInfo(Batch b)
		{
			lock (b)
			{
				BatchInfo binfo = new BatchInfo();
				binfo.BatchId = b.BatchId;
				binfo.BatchResources = new List<ResourceInfo>();
				foreach (BatchResource res in b.BatchResources)
				{
					if (System.IO.File.Exists(res.ServerPath))
					{
						ResourceInfo info = new ResourceInfo();
						info.DestName = res.ClientPath;
						info.TransferChunkSize = ServerNodeSettings.Default.TransferChunkSize;
						using (System.IO.FileStream fs = new System.IO.FileStream(res.ServerPath, System.IO.FileMode.Open))
						{
							byte[] hash = md5.ComputeHash(fs);
							info.Md5Hash = Convert.ToBase64String(hash);
							info.DestSize = fs.Length;
						}
						binfo.BatchResources.Add(info);
					}
				}
				binfo.Commands = new List<CommandInfo>();
				foreach (BatchCommand com in b.BatchCommands)
				{
					CommandInfo info = new CommandInfo();
					info.Arguments = com.Arguments;
					info.BreakOnException = com.BreakOnException;
					info.CaptureOutput = com.CaptureOutput;
					info.Command = com.Command;
					info.ErrorOnTimeOut = com.ErrorOnTimeOut;
					info.MaxTimeSeconds = com.MaxTimeSeconds;
					info.MinTimeSeconds = com.MinTimeSeconds;
					info.Path = com.Path;
					binfo.Commands.Add(info);
				}
				binfo.ReturnResources = new List<ResourceInfo>();
				foreach (BatchResource res in b.ReturnResources)
				{
					ResourceInfo info = new ResourceInfo();
					info.DestName = res.ClientPath;
					//info.BatchId = b.BatchId;
					binfo.ReturnResources.Add(info);
					info.TransferChunkSize = ServerNodeSettings.Default.TransferChunkSize;
				}

				return binfo;
			}
		}

		#endregion

		#region batch template

		/// <summary>
		/// Given the template, this applies the pattern to the base batch
		/// and adds the individual batches to the queue.
		/// 
		/// Adding a template also creates a new "batchgroup" -- that is all
		/// of the resulting batches are "grouped" together so their results
		/// are stored in the same directory.
		/// </summary>
		/// <param name="template"></param>
		/// <param name="comment"></param>
		public void AddBatchTemplate(BatchTemplate template, String comment)
		{
			if (comment == null)
				comment = String.Empty;

			BatchGroup bg = new BatchGroup();
			bg.Comment = comment;
			bg.StartTime = DateTime.Now;
			template.BaseBatch.BatchGroup = bg;

			// Long-term there is going to be a nice pattern model
			// until then, all I need is simple iteration
			Dictionary<String, String> variables = new Dictionary<string, string>();
			variables["i"] = "0";
			variables["j"] = "1";
			lock (batches)
			{
				for (int configIndex = template.StartConfigIndex; configIndex < template.EndConfigIndex; configIndex++)
				{
					variables["i"] = configIndex.ToString();
					variables["j"] = (configIndex + 1).ToString();
					Batch newBatch = new Batch(template.BaseBatch, variables);
					newBatch.QueueAdd = DateTime.Now;
					newBatch.NodeId = Guid.Empty;
					DataRow newBatchRow = batches.NewRow();
					newBatchRow[BatchTableKeys.BatchId] = newBatch.BatchId;
					newBatchRow[BatchTableKeys.BatchObject] = newBatch;
					batches.Rows.Add(newBatchRow);
					queuedBatches++;
					bg.TotalBatches++;
				}
			}
		}
		#endregion
	}
}
